iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0

寫測試可以幫助看 code 的人(可能是別人、也可能是未來的自己)明確暸解一個功能究竟能接受哪些參數、又會有哪幾種可能的輸出內容,也保障一個功能在「單元測試有覆蓋到的部分」是有品質保證的。為了能在晚上睡得更安穩,能從元件中獨立出來的邏輯就盡量提供配合的單元測試吧。

於是今天的主題就是:如何為白手起家的 React TypeScript app 專案設定 jest ( つ•̀ω•́)つ

安裝套件

在寫這篇鐵人賽文章的當下 jest 版本為 29.6,為了在這版 jest 順利把 TypeScript 測試跑起來,你需要安裝以下套件:

yarn add -D @jest/globals jest ts-jest ts-node

除了 jest 以外都是輔助 TypeScript 用的套件。如果在你閱讀這篇文章時 jest 的版本已遠超 29.6,建議在安裝完 jest 後,根據執行 yarn jest 後的終端訊息一個一個把需要的套件裝回去。

設定 jest.config.ts

在專案根目錄新增一個 jest.config.ts 檔案並填入以下內容:

import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom', // 選填,見下方說明
  moduleNameMapper: {
    '@/jest.config': '<rootDir>/jest.config.ts',
    '@Model/(.*)': '<rootDir>/src/model/$1',
    '@Tool/(.*)': '<rootDir>/src/tool/$1',
  },
};

export default config;

解說如下:

  • 根據官方文件說明,目前有兩種 preset 能讓 jest 能跑 TypeScript 測試,一個是 babel、另外一個就是今天使用的 ts-jest
  • 在測試腳本中如果有需要使用到瀏覽器環境 api(比如 document.createElement('div'))的話,記得將 testEnvironment 從預設的 node 改成 jsdom 並安裝套件 jest-environment-jsdom。來源:官方文件
  • 想要繼續在單元測試腳本中使用自定義的路徑(而非傳統的相對路徑)時,須設定 moduleNameMapper 讓 jest 知道如何解析自訂路徑
    • 以上方設定為例,這裡告訴 jest 在讀取到 @/jest.config 路徑時要去取 ./jest.config.ts、而在遇到 @Model@Tool 開頭的路徑時,要去 ./src/model./src/tool 資料夾尋找對應的內容
    • 注意這邊是透過 regex syntax 來比對出路徑,(.*) 比對到的字串會用於 $1,比如 @Tool/isValidUser 會對應到 <rootDir>/src/tool/isValidUser
    • .* 代表「任何字元都可以」且「允許長度從零到無限」,加上 () 代表要把比對出來的對象 group 起來,然後透過 $1 取出被 group 起來的內容

在執行 .ts 類型的單元測試時,以上的設定基本上就足夠了。

範例測試

首先在 ./src/tool 中新增了一個檔案 isValidUser.ts 並撰寫以下功能:

export function isValidUser(arg: unknown) {
  return (
    typeof arg === 'object' && !!arg && 'userName' in arg && !!arg.userName
  );
}

接著在 ./src/tool 中新增 isValidUser.test.ts 來撰寫測試腳本:

import { describe, expect, test } from '@jest/globals';
import { isValidUser } from '@Tool/isValidUser';

describe('isValidUser', () => {
  test('should return false if the argument is not an object', () => {
    expect(isValidUser(null)).toBe(false);
    expect(isValidUser(undefined)).toBe(false);
    expect(isValidUser(123)).toBe(false);
    expect(isValidUser('abc')).toBe(false);
    expect(isValidUser(true)).toBe(false);
    expect(isValidUser(false)).toBe(false);
  });
  test('should return false if the argument is an empty object', () => {
    expect(isValidUser({})).toBe(false);
  });
  test('should return false if the argument is an object without userName', () => {
    expect(isValidUser({ name: 'user A' })).toBe(false);
  });
  test('should return false if the argument is an object with empty userName', () => {
    expect(isValidUser({ userName: '' })).toBe(false);
  });
  test('should return true if the argument is an object with non-empty userName', () => {
    expect(isValidUser({ userName: 'user A' })).toBe(true);
  });
});

(純個人感受:GitHub Copilot 為簡單功能產生測試腳本的表現真的不錯,個人版方案一年 100 美金實在不算太貴,可以考慮課個金)

開啟終端,輸入 make test(參考第 6 天)即可執行單元測試:

 PASS  src/tool/isValidUser.test.ts
  isValidUser
    ✓ should return false if the argument is not an object (4 ms)
    ✓ should return false if the argument is an empty object
    ✓ should return false if the argument is an object without userName (1 ms)
    ✓ should return false if the argument is an object with empty userName
    ✓ should return true if the argument is an object with non-empty userName

單元測試的哲學

個人認為 Good Code, Bad Code 一書第十章「單元測試準則」對測試做了不錯的總結。目前在撰寫單元測試時,我會盡可能讓測試腳本都能符合以下特徵:

  1. 測試要能確實反映問題,且也不會產生誤報
  2. 以測試行為為主,不過份涉入實作細節
  3. 測試失敗時,應能提供具體的錯誤說明(發生在哪裡、哪一個項目失敗、收到的非預期結果又是什麼)
  4. 當其他工程師閱讀測試內容時,能夠理解「被測試的功能在做什麼」
  5. 容易被執行,比如透過終端即可觸發、不需要在每次測試時都需要大費周章地進行前置作業

附上個人之前的筆記,有較為細節的摘要,歡迎參考。

總結

對 TypeScript React app 專案設定單元測試的門檻並不高,多一點保險可以改善工程師的睡眠品質,真心推薦。


本文同步發佈於普通文組 2.0


上一篇
webpack 5 與圖片資源
下一篇
e2e 測試
系列文
捨棄 create-react-app 之餘還架了個 astro blog 昭告天下30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言